/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.anim.util
{
	import sweezy.anim.core.Animator;
	import sweezy.anim.core.IAction;
	import sweezy.anim.core.IAnimation;
	import sweezy.anim.core.IAnimationManager;
	import sweezy.anim.core.IEasing;
	import sweezy.anim.core.IPropertyAnimation;
	import sweezy.anim.easing.Easing;

	public class AnimationXMLTemplate
	{

		private static var _eventNames:Array = ["Complete", "Update", "Start", "Pause", "Error"];

		private static var _optionSubValueTypeMap:Object = {"string": null, "boolean": null, "int": null, "uint": null, "number": true};

		private static var _optionValueTypeMap:Object = {"array": null, "string": null, "boolean": null, "int": null, "uint": null, "number": true};

		private var _context:Object;

		private var _manager:IAnimationManager;

		private var _xml:XML;

		public function AnimationXMLTemplate(value:*)
		{
			if (value is XML)
			{
				_xml = value as XML;
			}
			else if (value !== null && value !== undefined)
			{
				try
				{
					_xml = new XML(String(value));
				}
				catch (e:Error)
				{
				}
			}
			else
			{
			}
		}

		public function createAndStartAnimation(context:Object, name:String = null):IAnimation
		{
			var animation:IAnimation = createAnimation(context);
			if (animation === null)
			{
				return null;
			}

			return animation.start(name);
		}

		public function createAnimation(context:Object):IAnimation
		{
			if (_xml === null)
			{
				return null;
			}

			_context = context;
			_manager = Animator.manager;

			var animation:IAnimation = getAnimation(_xml);
			_context = _manager = null;
			return animation;
		}

		private function actionHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			var action:IAction = getObject(node, "@action", IAction) as IAction;
			var quePosition:uint = getNumber(node, "@quePosition");
			return _manager.createActionAnimation(animation, action, quePosition);
		}

		private function animationHandler(node:XML):IAnimation
		{
			var element:Object = getObject(node, "@target", Object);
			if (element === null)
			{
				return null;
			}

			var duration:uint = getNumber(node, "@duration", 0);
			var delay:uint = getNumber(node, "@delay", 0);
			var easing:IEasing = getEasing(node);
			var animation:IPropertyAnimation = _manager.createPropertyAnimation(element, duration, easing, delay);
			animation.autoRewind = getBoolean(node, "@autoRewind");

			var option:Object = getObject(node, "@option", Object);
			for each (var optionNode:XML in node.option)
			{
				if (!("@prefix" in optionNode))
				{
					continue;
				}
				if (!("@name" in optionNode))
				{
					continue;
				}

				var value:* = getOptionValue(optionNode);
				if (value === null || value === undefined)
				{
					continue;
				}

				var prefix:String = String(optionNode.@prefix);
				if (option === null)
				{
					option = {};
				}

				var obj:Object = option[prefix];
				if (obj === null)
				{
					option[prefix] = obj = {};
				}
				obj[optionNode.@name] = value;
			}
			animation.option = option;

			for each (var propertyNode:XML in node.property)
			{
				if (!("@name" in propertyNode))
				{
					continue;
				}

				var to:Number = getNumber(propertyNode, "@to");
				var by:Number = getNumber(propertyNode, "@by");
				if (isNaN(to) && isNaN(by))
				{
					continue;
				}

				var name:String = String(propertyNode.@name);
				var round:Boolean = getBoolean(propertyNode, "@round");
				var from:Number = getNumber(propertyNode, "@from");

				if (isNaN(to))
				{
					animation.fromBy(name, from, by, round);
				}
				else
				{
					animation.fromBy(name, from, to, round);
				}
			}

			return animation;
		}

		private function easingHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			var easing:IEasing = getEasing(node);
			return _manager.createEasingAnimation(animation, easing);
		}

		private function emptyHandler(node:XML):IAnimation
		{
			var duration:uint = getNumber(node, "@duration", 0);
			return _manager.createEmptyAnimation(duration);
		}

		private function getAnimation(node:XML):IAnimation
		{
			var animation:IAnimation = null;

			switch (String(node.name()))
			{
				case "parallel":
					animation = parallelHandler(node);
					break;
				case "serial":
					animation = serialHandler(node);
					break;
				case "roundTrip":
					animation = roundTripHandler(node);
					break;
				case "reverse":
					animation = reverseHandler(node);
					break;
				case "scale":
					animation = scaleHandler(node);
					break;
				case "padding":
					animation = paddingHandler(node);
					break;
				case "slice":
					animation = sliceHandler(node);
					break;
				case "repeat":
					animation = repeatHandler(node);
					break;
				case "easing":
					animation = easingHandler(node);
					break;
				case "empty":
					animation = emptyHandler(node);
					break;
				case "animation":
					animation = animationHandler(node);
					break;
				case "action":
					animation = actionHandler(node);
					break;
			}

			if (animation !== null)
			{
				var tmp:String;
				for each (var name:String in _eventNames)
				{

					if (animation is IPropertyAnimation && name === "Error")
					{
						tmp = "@on" + name;
						if (tmp in node)
						{
							animation["on" + name] = getObject(node, tmp, Function);
						}
						tmp = "@on" + name + "Scope";
						if (tmp in node)
						{
							animation["on" + name + "Scope"] = getObject(node, tmp, Object);
						}
					}
					else
					{
						tmp = "@on" + name;
						if (tmp in node)
						{
							animation["on" + name] = getObject(node, tmp, Function);
						}
						tmp = "@on" + name + "Params";
						if (tmp in node)
						{
							animation["on" + name + "Params"] = getObject(node, tmp, Array);
						}
						tmp = "@on" + name + "Scope";
						if (tmp in node)
						{
							animation["on" + name + "Scope"] = getObject(node, tmp, Object);
						}
					}
				}
			}

			return animation;
		}

		private function getBoolean(node:XML, name:String):Boolean
		{
			var value:String = String(node[name]);
			return value.toLocaleLowerCase() === "true";
		}

		private function getChildAnimation(node:XML):IAnimation
		{
			var children:XMLList = node.children();
			var len:uint = children.length();
			if (len === 0)
			{
				return null;
			}

			var animation:IAnimation = null;
			for (var i:int = 0; i < len; i++)
			{
				animation = getAnimation(children[0]);
				if (animation !== null)
				{
					break;
				}
			}

			return animation;
		}

		private function getEasing(node:XML):IEasing
		{
			if ("@easing" in node)
			{
				var easing:Object = getObject(node, "@easing", Object);
				if (easing !== null)
				{
					if (easing is IEasing)
					{
						return easing as IEasing;
					}
					else if (easing is Function)
					{
						return Easing.convert(easing);
					}
					else
					{
						var es:IEasing = Easing.ease(String(easing));
						if (es !== null)
						{
							return es;
						}
					}
				}
			}

			if ("@ease" in node)
			{
				return Easing.ease(String(node.@ease));
			}

			return null;
		}

		private function getNumber(node:XML, name:String, defaultValue:Number = NaN):Number
		{
			var result:Number = parseFloat(String(node[name]));
			if (isNaN(result))
			{
				result = defaultValue;
			}

			return result;
		}

		private function getObject(node:XML, name:String, type:Class, defaultValue:* = null):Object
		{
			var name:String = String(node[name]);
			var result:Object = name === "this" ? _context : defaultValue;
			try
			{
				result = _context[name];
			}
			catch (e:Error)
			{
			}

			return result as type;
		}

		private function getOptionValue(node:XML):*
		{
			var valueNode:XML = node.*.(String(name()).toLowerCase() in _optionValueTypeMap)[0];
			if (valueNode === null)
			{
				return undefined;
			}

			var value:*;
			var name:String = String(valueNode.name()).toLocaleLowerCase();
			if (name === "array")
			{
				value = [];

				for each (var subValueNode:XML in node.*.(String(name()).toLowerCase() in _optionSubValueTypeMap))
				{
					var subValue:* = getOptionValue0(subValueNode);
					if (subValue === undefined)
					{
						continue;
					}

					value.push(subValue);
				}
			}
			else
			{
				value = getOptionValue0(subValueNode);
			}

			return value;
		}

		private function getOptionValue0(node:XML):*
		{
			var name:String = String(node.name()).toLocaleLowerCase();
			var value:String = node.text();
			if (name === "string")
			{
				return value;
			}
			else if (name === "boolean")
			{
				return value.toLocaleLowerCase() === "true";
			}

			var num:Number = parseFloat(value);
			if (name === "int")
			{
				return int(num);
			}
			else if (name === "uint")
			{
				return uint(num);
			}

			return num;
		}

		private function paddingHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			var before:uint = getNumber(node, "@before", 0);
			var after:uint = getNumber(node, "@after", 0);
			return _manager.createPaddingAnimation(animation, before, after);
		}

		private function parallelHandler(node:XML):IAnimation
		{
			var result:Array = [];
			for each (var child:XML in node.*)
			{
				var animation:IAnimation = getAnimation(child);
				if (animation === null)
				{
					continue;
				}

				result[result.length] = animation;
			}

			return _manager.createParallelAnimation(result);
		}

		private function repeatHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			var numRepeat:uint = getNumber(node, "@numRepeat", 0);
			return _manager.createRepeatAnimation(animation, numRepeat);
		}

		private function reverseHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			return _manager.createReverseAnimation(animation);
		}

		private function roundTripHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			return _manager.createRoundTripAnimation(animation);
		}

		private function scaleHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			var scale:Number = getNumber(node, "@scale", 1.0);
			return _manager.createScaledAnimation(animation, scale);
		}

		private function serialHandler(node:XML):IAnimation
		{
			var result:Array = [];
			for each (var child:XML in node.*)
			{
				var animation:IAnimation = getAnimation(child);
				if (animation === null)
				{
					continue;
				}

				result[result.length] = animation;
			}

			return _manager.createSerialAnimation(result);
		}

		private function sliceHandler(node:XML):IAnimation
		{
			var animation:IAnimation = getChildAnimation(node);
			if (animation === null)
			{
				return null;
			}

			var startPosition:uint = getNumber(node, "@startPosition", 0);
			var sliceDuration:uint = getNumber(node, "@sliceDuration", 0);
			return _manager.createSlicedAnimation(animation, startPosition, sliceDuration);
		}
	}
}
